require "serialport"

module Aio
	# This is the main class that communicates with the Arduino board.
	#
	# It implements several mechanisms like ACK/NAK for confirming
	# command execution and {http://en.wikipedia.org/wiki/Longitudinal_redundancy_check redundancy checks} 
	# to ensure data integrity.
	
	class Board
		# @example Modes of initialization
		#   # explicit
		#   board = Aio::Board.new("/dev/ttyUSB1")
		#   
		#   # block mode (closes the connection at the end)
		#   Aio::Board.new("/dev/ttyUSB0") do |board|
		#     # code...
		#   end
		#
		# @param port [String] the device/address used for communication.
		# @yield [self] an optional block of code to execute.
		# @raise [DeviceError] when the serial port is unavailable.
		
		def initialize(port)
			@serial = SerialPort.new(port, BAUDRATE, 8, 1, SerialPort::NONE)
		rescue Errno::ENOENT
			raise Aio::DeviceError, "Unavailable serial port at address => '#{port}'"
		else
			@serial.flow_control = SerialPort::NONE
			@serial.read_timeout = TIMEOUT
			@serial.sync = true
			
			if block_given?
				yield(self)
				close
			end
		end
		
		# Closes the connection with the board.
		#
		# @return [nil]
		
		def close
			@serial.close
			nil
		end
		
		# Sets the mode of operation for a specific pin.
		#
		# @param pin [Integer] pin number (as stated in the board).
		# @param mode [Integer] mode of operation: {INPUT}, {OUTPUT} or {INPUT_PULLUP}.
		# @return [Boolean] if the command was successful.
		# @raise [DeviceError] on unexpected device errors.
		
		def pinMode(pin, mode)
			try(false) {
				sendCMD [1, pin, mode]
				onACK {true}
			}
		end
		
		# Reads the value from a specified digital pin.
		#
		# @param pin [Integer] pin number (as stated in the board).
		# @return [Integer, nil] {HIGH} (1), {LOW} (0) or *nil* on error.
		# @raise (see #pinMode)
		
		def digitalRead(pin)
			try(nil) {
				sendCMD [2, pin, empty]
				onACK do
					onData(4) do |bytes|
						bytes[0]
					end
				end
			}
		end
		
		# Writes a {HIGH} (1) or a {LOW} (0) value to a digital pin. 
		#
		# @param pin [Integer] pin number (as stated in the board).
		# @param state [Integer] {HIGH} (1) or {LOW} (0).
		# @return (see #pinMode)
		# @raise (see #pinMode)
		
		def digitalWrite(pin, state)
			try(false) {
				sendCMD [3, pin, state]
				onACK {true}
			}
		end
		
		# Reads the value from a specified analog pin.
		#
		# @param pin [Integer] pin number (as stated in the board).
		# @return [Integer, nil] an integer between *0* and *1023* or *nil* on error.
		# @raise (see #pinMode)
		
		def analogRead(pin)
			try(nil) {
				sendCMD [4, pin, empty]
				onACK do
					onData(4) do |bytes|
						bytes[0] + (bytes[1] << 8)
					end
				end
			}
		end
		
		# Writes an analog value (PWM wave) to a pin.
		#
		# @param pin [Integer] pin number (as stated in the board).
		# @param value [Integer] an integer between *0* (always off) and *255* (always on).
		# @return (see #pinMode)
		# @raise (see #pinMode)
		
		def analogWrite(pin, value)
			try(false) {
				sendCMD [5, pin, value]
				onACK {true}
			}
		end
		
		# Configures the reference voltage used for analog input.
		#
		# @param type [Integer] {DEFAULT}, {EXTERNAL}, {INTERNAL}, {INTERNAL1V1} or {INTERNAL2V56}.
		# @return (see #pinMode)
		# @raise (see #pinMode)
		
		def analogReference(type)
			try(false) {
				sendCMD [6, type, empty]
				onACK {true}
			}
		end
		
		# Returns the number of milliseconds since the Arduino board began running the current program.
		#
		# @return [Integer, nil] a *32bit* *integer* or *nil* on error.
		# @raise (see #pinMode)
		
		def millis
			try(nil) {
				sendCMD [7, empty, empty]
				onACK do
					onData(4) do |bytes|
						bytes[0] + (bytes[1] << 8) + (bytes[2] << 16) + (bytes[3] << 24)
					end
				end
			}
		end
		
		# Tells if the board is ready.
		#
		# @return [Boolean]
		# @raise (see #pinMode)
		
		def ready?
			try(false) {
				sendCMD [8, empty, empty]
				onACK {true}
			}
		end
		
		private
		
		def try(ret, &block)
			instance_eval(&block)
		rescue => e
			@serial.flush_input
			case e
				when EOFError, Aio::CommandError
					return ret
				when Errno::EIO
					raise(Aio::DeviceError, e.message)
				else
					raise
			end
		end
		
		def onACK(&block)
			if @serial.readbyte == ACK
				instance_eval(&block)
			else
				raise Aio::CommandError
			end
		end
		
		def onData(n, &block)
			bytes = readBytes(n)
			lrc = @serial.readbyte
			if lrc == getLRC(bytes)
				instance_exec(bytes, &block)
			else
				raise Aio::CommandError
			end
		end
		
		def sendCMD(ary)
			@serial.putc(SYNCBYTE)
			ary.each {|byte| @serial.putc(byte.to_i)}
			@serial.putc(getLRC(ary))
		end
		
		def readBytes(n)
			bytes = []
			n.times {bytes << @serial.readbyte}
			bytes
		end
		
		def getLRC(ary)
			lrc = ary.inject(0) {|acc,val| (acc + (val.to_i & 0xff)) & 0xff}
			(lrc ^ 0xff) + 1
		end
		
		def empty
			rand(256)
		end
	end
end
